/*
 * Copyright (c) 2010-2011 Josef Hardi <josef.hardi@gmail.com>
 * Copyright (c) 2010 Piotr Zagawa <piotr.zagawa@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package id.duapuluhdua.dakidaki.map;

import java.util.List;

/**
 * The rectangular region that covers all {@link GeoPoint} in the screen.
 */
public class Viewport
{
  /** Constants */
  public final static Integer MODE_FREE = 0;
  public final static Integer MODE_TRACKS = 1;
  public final static Integer MODE_LOCATION = 2;

  /** Fields */
  private static double defaultRadius = 500.0; // meters
  private static double marginScale = 0.2;

  private double mScreenAspectRatio = 1;

  private GeoPoint mMin = new GeoPoint();
  private GeoPoint mMax = new GeoPoint();

  /**
   * Define the screen aspect ratio.
   *
   * @param screenAspectRatio the ratio between the sizes of width and height.
   */
  public void setScreenAspectRatio(double screenAspectRatio)
  {
    mScreenAspectRatio = screenAspectRatio;
  }

  /**
   * Set the minimum and maximum boundaries where the input {@link GeoPoint}
   * serves as the center of the default radius.
   *
   * @param point the input geo-point.
   */
  public void reset(final GeoPoint point)
  {
    final double dHalfRadius = defaultRadius / 2;

    mMin.mercX = point.mercX - dHalfRadius;
    mMin.mercY = point.mercY - dHalfRadius;
    mMax.mercX = point.mercX + dHalfRadius;
    mMax.mercY = point.mercY + dHalfRadius;

    updateAspect();
    addMargin(marginScale);
  }

  /**
   * Set the minimum and maximum boundaries by selecting which {@link GeoPoint}
   * is lesser (i.e., the minimum) than the other (i.e., the maximum).
   *
   * @param point1 the first input geo-point.
   * @param point2 the second input geo-point.
   */
  public void reset(final GeoPoint point1, final GeoPoint point2)
  {
    mMin.mercX = Math.min(point1.mercX, point2.mercX);
    mMin.mercY = Math.min(point1.mercY, point2.mercY);
    mMax.mercX = Math.max(point1.mercX, point2.mercX);
    mMax.mercY = Math.max(point1.mercY, point2.mercY);

    updateAspect();
    addMargin(marginScale);
  }

  /**
   * Set the minimum and maximum boundaries by searching through the whole
   * list of the {@link GeoPoint}.
   *
   * @param points the list of geo-points.
   */
  public void reset(final List<GeoPoint> points)
  {
    if (points.size() == 0) {
      return;
    }
    if (points.size() == 1) {
      reset(points.get(0));
      return;
    }
    if (points.size() == 2) {
      reset(points.get(0), points.get(1));
      return;
    }

    GeoPoint pointBegin = points.get(0);
    mMin.mercX = pointBegin.mercX;
    mMin.mercY = pointBegin.mercY;
    mMax.mercX = pointBegin.mercX;
    mMax.mercY = pointBegin.mercY;

    for (int i = 0; i < points.size(); i++) {
      final GeoPoint point = points.get(i);

      // Find the most minimum value
      if (point.mercX < mMin.mercX) {
        mMin.mercX = point.mercX;
      }
      if (point.mercY < mMin.mercY) {
        mMin.mercY = point.mercY;
      }

      // Find the most maximum value
      if (point.mercX > mMax.mercX) {
        mMax.mercX = point.mercX;
      }
      if (point.mercY > mMax.mercY) {
        mMax.mercY = point.mercY;
      }
    }
    updateAspect();
    addMargin(marginScale);
  }

  /**
   * Move a point to a specific distance.
   *
   * @param metersX
   * @param metersY
   */
  public void shift(double metersX, double metersY)
  {
    mMin.mercX += metersX;
    mMin.mercY += metersY;
    mMax.mercX += metersX;
    mMax.mercY += metersY;
  }

  /**
   * Return the measure of the left-most of the viewport.
   *
   * @return a unit of length in the mercator projection (m).
   */
  public double getMinX()
  {
    return mMin.mercX;
  }

  /**
   * Return the measure of the right-most of the viewport.
   *
   * @return A unit of length in the mercator projection (m).
   */
  public double getMaxX()
  {
    return mMax.mercX;
  }

  /**
   * Return the measure of the top-most of the viewport.
   *
   * @return A unit of length in the mercator projection (m).
   */
  public double getMinY()
  {
    return mMin.mercY;
  }

  /**
   * Return the measure of the bottom-most of the viewport.
   *
   * @return A unit of length in the mercator projection (m).
   */
  public double getMaxY()
  {
    return mMax.mercY;
  }

  /**
   * Return the width of the region.
   *
   * @return The width value.
   */
  public double getWidth()
  {
    return (mMax.mercX - mMin.mercX);
  }

  /**
   * Return the height of the region.
   *
   * @return the height value.
   */
  public double getHeight()
  {
    return (mMax.mercY - mMin.mercY);
  }

  /**
   * The ratio between the width and height of the rectangular region.
   *
   * @return the ratio.
   */
  public double getAspectRatio()
  {
    if (getHeight() == 0) {
      return 0;
    }
    return getWidth() / getHeight();
  }

  /**
   * Add (or remove) spaces from the current viewport size.
   *
   * @param metersX
   *            Extra space for the width (x-axis).
   * @param metersY
   *            Extra space for the height (y-axis).
   * @return Returns true if the size of the space does not exceed the size of
   * the width or the height, in case of removing the spaces.
   */
  public boolean addMargin(double metersX, double metersY)
  {
    if (getWidth() + (metersX * 2) < 0) {
      return false;
    }
    if (getHeight() + (metersY * 2) < 0) {
      return false;
    }
    mMin.mercX -= metersX;
    mMin.mercY -= metersY;
    mMax.mercX += metersX;
    mMax.mercY += metersY;

    return true;
  }

  /**
   * Add space from the current viewport size using a scale.
   *
   * @param scale
   */
  private void addMargin(double scale)
  {
    addMargin((getWidth() * scale), (getHeight() * scale));
  }

  /**
   * Adjust the viewport size to the screen size so both have the same
   * aspect ratio.
   */
  private void updateAspect()
  {
    if (mScreenAspectRatio > getAspectRatio()) {
      final double dDiffW = (mScreenAspectRatio * getHeight()) - getWidth();
      addMargin(dDiffW * 0.5, 0); // add margin to the width
    }
    else if (mScreenAspectRatio < getAspectRatio()) {
      final double dDiffH = (getWidth() / mScreenAspectRatio) - getHeight();
      addMargin(0, dDiffH * 0.5);  // add margin to the height
    }
  }
}
